package jgf.lists;

import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import jgf.Arc;
import jgf.Attribute;
import jgf.IGraph;
import jgf.Node;
import jgf.beans.ArcBean;
import jgf.beans.NodeBean;
import jgf.exceptions.arc.ArcException;
import jgf.exceptions.arc.DuplicatedArcException;
import jgf.exceptions.arc.UnexistentArcException;
import jgf.exceptions.attribute.AttributeException;
import jgf.exceptions.attribute.BadAttributeListException;
import jgf.exceptions.attribute.DuplicatedAttributeException;
import jgf.exceptions.attribute.UnexistentAttributeException;
import jgf.exceptions.node.DuplicatedNodeException;
import jgf.exceptions.node.UnexistentNodeException;

/**
 * This class is the representative class of any graph.
 * All the storing fields and algorithms are based on Balanced Tree Maps.
 * In this way, almos all the acceses to the insides of the graph
 * are O(log) or O(const).
 * @author Marcos Garcia Ortega <a href="mailto:thiamath@gmail.com">&lt;thiamath@gmail.com&gt;</a>
 */
public abstract class GenericGraph implements IGraph{

	////////////
	// Fields //
	////////////

	// <editor-fold defaultstate="collapsed" desc="Fields">
	/** The type of the serialized graph. */
	protected int type = 0;
	/** The ID number that identifies the graph. */
	protected Integer id;
	/** A name assigned to the graph. */
	protected String label;
	/** Keeps the id of the nodes to assign them incrementally and univocally to the nodes. */
	protected int nodeIdCounter = 1;
	/** Stores all the nodes of the graph and keeps them indexed by Its ID number. */
	protected TreeMap<Integer, Node> nodeTree;
	/** Stores all the attributes and its default values related to nodes. */
	protected TreeMap<String, Attribute> nodeDefaultAttributes;
	/** Stores a reference to the nodes that are sink on the graph. */
	protected TreeMap<Integer, Node> sinks;
	/** Stores a reference to the nodes that are source on the graph. */
	protected TreeMap<Integer, Node> sources;
	/** Keeps the id of the arcs to assign them incrementally and univocally to the nodes. */
	protected int arcIdCounter = 1;
	/** Stores all the arcs of the graph and keeps them indexed by its ID number. */
	protected TreeMap<Integer, Arc> arcTree;
	/** Stores all the attributes and its default values related to arcs. */
	protected TreeMap<String, Attribute> arcDefaultAttributes;
	// </editor-fold>

	//////////////////////
	// Graph management //
	//////////////////////

	// <editor-fold defaultstate="collapsed" desc="Constructors">
	/**
	 * Constructor that generates an empty Generic graph
	 * with an invalid ID number.
	 * This constructor should never be used.
	 */
	protected GenericGraph(){
		this(-1);
	}

	/**
	 * Constructor that generates a Generic graph that have an empty label.
	 * @param id The ID number of the graph.
	 */
	public GenericGraph(Integer id) {
		this(id, "");
	}

	/**
	 * Constructor that generates a Generic graph.
	 * @param id The ID number of the graph.
	 * @param label The label of the graph.
	 */
	public GenericGraph(Integer id, String label){
		this.id = id;
		this.label = label;
		this.nodeTree = new TreeMap<Integer, Node>();
		this.nodeDefaultAttributes = new TreeMap<String, Attribute>();
		this.sinks = new TreeMap<Integer, Node>();
		this.sources = new TreeMap<Integer, Node>();
		this.arcTree = new TreeMap<Integer, Arc>();
		this.arcDefaultAttributes = new TreeMap<String, Attribute>();
	}
	// </editor-fold>

	// <editor-fold defaultstate="collapsed" desc="Getters">

	public String getName() {
		return label;
	}

	public Integer getID() {
		return id;
	}

	public int getType() {
		return type;
	}
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Setters">
	public void setID(Integer id){
		this.id = id;
	}

	public void setName(String name) {
		this.label = name;
	}
	// </editor-fold>

	/////////////////////
	// Node management //
	/////////////////////

	// <editor-fold defaultstate="collapsed" desc="Node management">

	public Integer insertNode(String label) {
		nodeTree.put(nodeIdCounter, new Node(nodeIdCounter, label, null));
		//Put the new node with the given label.
		return nodeIdCounter++;
	}

	public void deleteNode(Integer nodeId) throws UnexistentNodeException {
		Node remove = nodeTree.remove(nodeId);
		//Remove the node.
		if (remove == null) {
			//If the removed node is null
			throw new UnexistentNodeException(this.getClass(), "deleteNode", new String[]{"nodeId = " + nodeId});
		}
	}

	public void updateNode(Integer nodeID, String newLabel, List<Attribute> newAttributes)
			throws UnexistentNodeException,
			BadAttributeListException {
		Node node = nodeTree.get(nodeID);
		if(node == null)
			throw new UnexistentNodeException(this.getClass(),
				"updateNode",
				new String[]{
				"nodeID = " + nodeID,
				"newLabel = " + newLabel,
				"newAttributes = " + newAttributes});
		if(newLabel != null)
			node.setLabel(newLabel);
		if(this.validateNodeAttributeList(newAttributes))
			for (Attribute attribute : newAttributes) {
				try {
					node.updateAttribute(attribute.getName(), attribute);
				} catch (AttributeException ex) {
					throw new BadAttributeListException(this.getClass(),
						"updateNode",
						new String[]{
						"nodeID = " + nodeID,
						"newLabel = " + newLabel,
						"newAttributes = " + newAttributes});
				}
			}
		else
			throw new BadAttributeListException(this.getClass(),
				"updateNode",
				new String[]{
				"nodeID = " + nodeID,
				"newLabel = " + newLabel,
				"newAttributes = " + newAttributes});
	}

	public NodeBean selectNode(Integer nodeId) throws UnexistentNodeException {
		Node node = nodeTree.get(nodeId);
		//Get the node.
		if (node == null) {
			//If the selected node is null
			throw new UnexistentNodeException(this.getClass(), "selectNode", new String[]{"nodeId = " + nodeId});
		}
		//Throw the UnexistentNodeException.
		return new NodeBean(node);
	}

	public List<Integer> getNodeList() {
		ArrayList<Integer> ret = new ArrayList<Integer>();
		//Set the return list.
		for (Integer nodeId : nodeTree.keySet()) {
			//For each node in the graph
			ret.add(nodeTree.get(nodeId).getId());
		}
		return ret;
	}

	public List<Attribute> getNodeAttributes(Integer nodeId) throws UnexistentNodeException {
		return nodeTree.get(nodeId).getAttributeList();
	}

	public void setSink(Integer nodeID)
			throws DuplicatedNodeException,
			UnexistentNodeException {
		if (sinks.containsKey(nodeID)) {//If the node is already in the sources list
			throw new DuplicatedNodeException(this.getClass(),
					"setSink",
					new String[]{"nodeID = " + nodeID});//throw the DuplicatedNodeException.
		}
		if (!nodeTree.containsKey(nodeID)){//If the node do not exist in the graph
			throw new UnexistentNodeException(this.getClass(),
					"setSink",
					new String[]{"nodeID = " + nodeID});//throw the UnexistentNodeException.
		}
		sources.put(nodeID, nodeTree.get(nodeID));//Set the node as source.
	}

	public void setSource(Integer nodeID)
			throws DuplicatedNodeException,
			UnexistentNodeException {
		if (sources.containsKey(nodeID)) {//If the node is already in the sources list
			throw new DuplicatedNodeException(this.getClass(),
					"setSource",
					new String[]{"nodeID = " + nodeID});//throw the DuplicatedNodeException.
		}
		if (!nodeTree.containsKey(nodeID)){//If the node do not exist in the graph
			throw new UnexistentNodeException(this.getClass(),
					"setSource",
					new String[]{"nodeID = " + nodeID});//throw the UnexistentNodeException.
		}
		sources.put(nodeID, nodeTree.get(nodeID));
	}

	public void unsetSink(Integer nodeId)
			throws UnexistentNodeException {
		if (sinks.remove(nodeId) == null) {
			//If the node is not on the list
			throw new UnexistentNodeException(this.getClass(), "unsetSink", new String[]{"nodeId = " + nodeId});
		}
	}

	public void unsetSource(Integer nodeId)
			throws UnexistentNodeException {
		if (sources.remove(nodeId) == null) {
			//If the node is not on the list
			throw new UnexistentNodeException(this.getClass(), "unsetSource", new String[]{"nodeId = " + nodeId});
		}
	}

	public List<Integer> getSinks() {
		ArrayList<Integer> ret = new ArrayList<Integer>();
		//Set the return value.
		for (Integer nodeId : sinks.keySet()) {
			//For each node in the sinks tree
			ret.add(nodeId);
		}
		return ret;
	}

	public List<Integer> getSources() {
		ArrayList<Integer> ret = new ArrayList<Integer>();
		//Set the return value.
		for (Integer nodeId : sources.keySet()) {
			//For each node in the sources tree
			ret.add(nodeId);
		}
		return ret;
	}
	// </editor-fold>

	////////////////////
	// Arc management //
	////////////////////

	// <editor-fold defaultstate="collapsed" desc="Arc management">
	public abstract Integer insertArc(Integer from, Integer to, String label)
		throws ArcException,
		DuplicatedArcException,
		DuplicatedNodeException,
		UnexistentNodeException;

	public void deleteArc(Integer arcID) throws UnexistentArcException{
		if(arcTree.remove(arcID) == null)
			throw new UnexistentArcException(this.getClass(),
				"updateArc",
				new String[]{
				"arcID = " + arcID});
	}
	
	public void updateArc(Integer arcID, String newLabel, List<Attribute> newAttributes)
			throws UnexistentArcException,
			BadAttributeListException {
		Arc arc = arcTree.get(arcID);
		if(arc == null)
			throw new UnexistentArcException(this.getClass(),
				"updateArc",
				new String[]{
				"arcID = " + arcID,
				"newLabel = " + newLabel,
				"newAttributes = " + newAttributes});
		if(newLabel != null)
			arc.setLabel(newLabel);
		if(this.validateArcAttributeList(newAttributes))
			for (Attribute attribute : newAttributes) {
				try {
					arc.updateAttribute(attribute.getName(), attribute);
				} catch (AttributeException ex) {
					throw new BadAttributeListException(this.getClass(),
						"updateArc",
						new String[]{
						"arcID = " + arcID,
						"newLabel = " + newLabel,
						"newAttributes = " + newAttributes});
					}
			}
		else
			throw new BadAttributeListException(this.getClass(),
				"updateArc",
				new String[]{
				"arcID = " + arcID,
				"newLabel = " + newLabel,
				"newAttributes = " + newAttributes});
	}

	public ArcBean selectArc(Integer arcID)
			throws UnexistentArcException{
		ArcBean ret = new ArcBean(arcTree.get(arcID));
		ret.setGraphId(this.getID());
		return ret;
	}

	public List<Integer> getArcList() {
		List<Integer> ret = new ArrayList<Integer>();

		for (Integer key : arcTree.keySet()) {
			ret.add(arcTree.get(key).getId());
		}

		return ret;
	}

	public abstract List<Integer> getBackwardStar(Integer nodeId);

	public abstract List<Integer> getForwardStar(Integer nodeId);

	public List<Attribute> getArcAttributes(Integer arcID) {
		return  arcTree.get(arcID).getAttributeList();
	}

	public List<Integer> getMultiarcList(Integer nodeFromID, Integer nodeToID)
			throws UnexistentArcException{
		List<Integer> ret = new ArrayList<Integer>();
		for (Integer key : arcTree.keySet()) {
			if(arcTree.get(key).getFrom().getId().equals(nodeFromID)
					&& arcTree.get(key).getTo().getId().equals(nodeToID))
				ret.add(key);
		}
		return ret;
	}

	public List<Integer> getStar(Integer nodeID) {
		List<Integer> ret = new ArrayList<Integer>();

		for (Arc arc : nodeTree.get(nodeID).getLinkedArcs()) {
			ret.add(arc.getId());
		}

		return ret;
	}
	// </editor-fold>

	//////////////////////////
	// Attribute management //
	//////////////////////////

	// <editor-fold defaultstate="collapsed" desc="Attribute management">

	public void addDefaultArcAttribute(Attribute defaultAttribute)
		throws DuplicatedAttributeException {
		if (arcDefaultAttributes.containsKey(defaultAttribute.getName())) {//If the current list of attributes contains the given attribute
			throw new DuplicatedAttributeException(this.getClass(),
				"addDefaultArcAttribute",
				new String[]{"defaultValue = " + defaultAttribute});//Throw the DuplicatedAttributeException
		}

		this.arcDefaultAttributes.put(defaultAttribute.getName(), defaultAttribute);//Add the new attribute to the list
		for (Integer key : arcTree.keySet()) {//For each arc in the arc tree
			Arc arc = arcTree.get(key);//get the arc
			arc.insertAttribute(defaultAttribute);//add the attribute to the arc
		}
	}

	public void addDefaultNodeAttribute(Attribute defaultAttribute)
		throws DuplicatedAttributeException {
		if (nodeDefaultAttributes.containsKey(defaultAttribute.getName())) {//If the current list of attributes contains the given attribute
			throw new DuplicatedAttributeException(this.getClass(),
				"addDefaultNodeAttribute",
				new String[]{"defaultAttribute = " + defaultAttribute});//Throw the DuplicatedAttributeException
		}

		this.nodeDefaultAttributes.put(defaultAttribute.getName(), defaultAttribute);//Add the new attribute to the list
		for (Integer key : nodeTree.keySet()) {//For each node in the node tree
			Node node = nodeTree.get(key);//get the node
			node.insertAttribute(defaultAttribute);//add the attribute to the node
		}
	}

	public void removeDefaultArcAttribute(String attributeName)
		throws UnexistentAttributeException {
		if (arcDefaultAttributes.remove(attributeName) == null) {
			throw new UnexistentAttributeException(this.getClass(), "removeDefaultArcAttribute", new String[]{"attributeName = " + attributeName});
		}
	}

	public void removeDefaultNodeAttribute(String attributeName)
		throws UnexistentAttributeException {
		if (nodeDefaultAttributes.remove(attributeName) == null) {//If the default list of attributes do not contain this attribute
			throw new UnexistentAttributeException(this.getClass(), "removeDefaultNodeAttribute", new String[]{"attributeName = " + attributeName});//Throw the UnexistentAttributeException.
		}
	}

	public void updateDefaultArcAttribute(String previousName, Attribute newValue)
		throws AttributeException,
		DuplicatedAttributeException,
		UnexistentAttributeException {
		if (previousName == null || newValue == null) {//If the previous name or the new attribute are null
			throw new AttributeException(this.getClass(),
					"updateDefaultArcAttribute",
					new String[]{"previousName = " + previousName,
						"newValue = " + newValue});//Throw a AttributeException
		}
		if (!arcDefaultAttributes.containsKey(previousName)) {//If the previous attribute do not exist
			throw new UnexistentAttributeException(this.getClass(),
					"updateDefaultArcAttribute",
					new String[]{"previousName = " + previousName,
						"newValue = " + newValue});//Throw a UnexistentAttributeException
		}
		if (!previousName.equalsIgnoreCase(newValue.getName())) {//If the new name is different than the previous one
			if (arcDefaultAttributes.containsKey(newValue.getName())) {
				throw new DuplicatedAttributeException(this.getClass(),
						"updateDefaultArcAttribute",
						new String[]{"previousName = " + previousName,
							"newValue = " + newValue});//Throw a DuplicatedAttributeException
			} else {
				arcDefaultAttributes.remove(previousName);//remove the previous attribute
			}
		}
		arcDefaultAttributes.put(newValue.getName(), newValue);//Put or replace the new attribute
	}

	public void updateDefaultNodeAttribute(String previousName, Attribute newValue)
		throws AttributeException,
		DuplicatedAttributeException,
		UnexistentAttributeException {
		if (previousName == null || newValue == null) {//If the previous name or the new attribute are null
			throw new AttributeException(this.getClass(),
					"updateDefaultNodeAttribute",
					new String[]{"previousName = " + previousName,
						"newValue = " + newValue});//Throw a AttributeException
		}
		if (!nodeDefaultAttributes.containsKey(previousName)) {//If the previous attribute do not exist
			throw new UnexistentAttributeException(this.getClass(),
					"updateDefaultNodeAttribute",
					new String[]{"previousName = " + previousName,
						"newValue = " + newValue});//Throw a UnexistentAttributeException
		}
		if (!previousName.equalsIgnoreCase(newValue.getName())) {//If the new name is different than the previous one
			if (nodeDefaultAttributes.containsKey(newValue.getName())) {
				throw new DuplicatedAttributeException(this.getClass(),
						"updateDefaultNodeAttribute",
						new String[]{"previousName = " + previousName,
							"newValue = " + newValue});//Throw a DuplicatedAttributeException
			} else {
				nodeDefaultAttributes.remove(previousName);//remove the previous attribute
			}
		}
		nodeDefaultAttributes.put(newValue.getName(), newValue);//Put or replace the new attribute
	}

	public List<Attribute> getDefaultArcAttributes() {
		ArrayList<Attribute> ret = new ArrayList<Attribute>();//Set the return list.

		for (String key : arcDefaultAttributes.keySet()) {//For each attribute in arcDefaultAttributes
			ret.add(arcDefaultAttributes.get(key));//add the attribute tu the return list.
		}

		return ret;//Return the list.
	}

	public List<Attribute> getDefaultNodeAttributes() {
		ArrayList<Attribute> ret = new ArrayList<Attribute>();//Set the return list.

		for (String key : nodeDefaultAttributes.keySet()) {//For each attribute in nodeDefaultAttributes
			ret.add(nodeDefaultAttributes.get(key));//add the attribute tu the return list.
		}
		
		return ret;//Return the list.
	}

	public boolean validateArcAttributeList(List<Attribute> attributes) {
		for (Attribute attribute : attributes) {
			if(!arcDefaultAttributes.containsKey(attribute.getName()))
				return false;
		}
		return true;
	}

	public boolean validateNodeAttributeList(List<Attribute> attributes) {
		for (Attribute attribute : attributes) {
			if(!nodeDefaultAttributes.containsKey(attribute.getName()))
				return false;
		}
		return true;
	}
	// </editor-fold>
}
